﻿//*** Guidance generated code ***//
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.Mvc;

using ProjectBase.Core;
using ProjectBase.Core.Service;

namespace ProjectBase.Mvc.Controllers
{
    /// <summary>
    ///     Extends the basic Binder results.
    ///     In comparison with the base implementation,
    ///     this will convert string into 'decimal', 'int', 'short', 'long', 'Guid', 'DateTime', 'float', 'double'
    ///     based on the User culture.
    ///     Together with "oposite feature" 'ToDisplay()', which renders value types in culture specific strings,
    ///     provides user friendly input-output conversions.
    ///     (e.g. '1 000' thousand separator is working!)
    /// </summary>
    public class SmartBinder : DefaultModelBinder
    {
        #region GetProperty
        protected override object GetPropertyValue(ControllerContext controllerContext,
            ModelBindingContext bindingContext,
            PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
        {
            // no value provided
            var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (value.IsNull())
            {
                return null;
            }

            // value Type are evaluated with the cultur specific settings (e.g. 1 000 with a thousand separator)
            if (propertyDescriptor.PropertyType.IsValueType)
            {
                ValueType result;
                if (value.AttemptedValue.TryParse(value.Culture, propertyDescriptor.PropertyType, out result))
                {
                    return result;
                }
            }

            // strings are no longer converted into NULL (overrides the default)
            else if (propertyDescriptor.PropertyType.IsEquivalentTo(typeof(string)))
            {
                string fileName;
                // try to set the string property ending with "FileName"
                if (GetFileName(bindingContext, out fileName))
                {
                    return fileName;
                }
                if (value.AttemptedValue.IsEmpty())
                {
                    return string.Empty;
                }
            }

            // tries to fill byte[] with the HttpPostedFile
            else if (propertyDescriptor.PropertyType.IsEquivalentTo(typeof(byte[])))
            {
                var files = value.RawValue as HttpPostedFileBase[];
                if (files.IsNotEmpty())
                {
                    return GetFile(files);
                }
            }

            // is property entity - IPersistentObject?
            else if (propertyDescriptor.PropertyType
                .GetInterfaces().Any(i => i.IsEquivalentTo(typeof(IPersistentObject))))
            {
                return ResolveIPersitent(propertyDescriptor, value);
            }

            // is property IList<entity> of a IPersitentObject type ?
            if (propertyDescriptor.PropertyType.IsGenericType
              && propertyDescriptor.PropertyType.GetGenericTypeDefinition() == typeof(IList<>)
              && propertyDescriptor.PropertyType.GetGenericArguments()[0]
                .GetInterfaces().Any(i => i.IsEquivalentTo(typeof(IPersistentObject))))
            {
                return ResolveIPersistentList(bindingContext, propertyDescriptor, value);
            }

            // the rest is left on the default implementation e.g. IList<int>
            return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
        }

        protected virtual byte[] GetFile(HttpPostedFileBase[] files)
        {
            Contract.Requires(files.Is());

            var httpPostedFileBase = files[0];
            if (httpPostedFileBase.IsNull())
            {
                return null;
            }
            var buffer = new byte[httpPostedFileBase.ContentLength];
            httpPostedFileBase.InputStream.Read(buffer, 0, httpPostedFileBase.ContentLength);
            return buffer;
        }

        /// <summary>
        /// If there is a byte[] property named e.g. "xxx" and also the string property
        /// named the same with suffix "FileName": e.g. "xxxFileName",
        /// binder will bind the file to the byte[] property "xxx" and use the file name
        /// for property "xxxFileName"
        /// </summary>
        /// <param name="bindingContext"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        protected virtual bool GetFileName(ModelBindingContext bindingContext, out string fileName)
        {
            Contract.Requires(bindingContext.Is());

            fileName = null;
            if (bindingContext.ModelName.EndsWith("FileName"))
            {
                var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName.Replace("FileName", string.Empty));
                if (value.Is())
                {
                    var files = value.RawValue as HttpPostedFileBase[];
                    if (files.IsNotEmpty()
                        && files[0].Is())
                    {
                        fileName = files[0].FileName.Split('\\').Last();
                    }
                    return true;
                }
            }
            return false;
        }
        #endregion GetProperty

        #region Resolve Persistent
        protected virtual IPersistentObject ResolveIPersitent(PropertyDescriptor propertyDescriptor, ValueProviderResult value)
        {
            Contract.Requires(propertyDescriptor.Is());
            Contract.Requires(value.Is());

            var persistentResults = FindIPersistentObjects(propertyDescriptor.PropertyType, value);
            return persistentResults.IsNotEmpty()
                 ? persistentResults.First()
                 : null;
        }
        protected virtual IEnumerable<IPersistentObject> ResolveIPersistentList(ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, ValueProviderResult value)
        {
            Contract.Requires(bindingContext.Is());
            Contract.Requires(propertyDescriptor.Is());
            Contract.Requires(value.Is());
            Contract.Requires(propertyDescriptor.PropertyType.IsGenericType, "Call this method only for Model == IList<Entity>");
            Contract.Requires(propertyDescriptor.PropertyType.GetGenericTypeDefinition() == typeof(IList<>), "Call this method only for Model == IList<Entity>");

            // Caset Model to IEnumerable to use some LINQ without reflection
            var model = bindingContext.Model as IEnumerable<IPersistentObject>;
            // the model is generic list, so take first (and only) argument
            var propertyType = propertyDescriptor.PropertyType.GetGenericArguments()[0];

            if (model.IsNull()) // property was not instantiated
            {
                var listType = typeof(List<>);
                var constructedListType = listType.MakeGenericType(propertyType);
                model = Activator.CreateInstance(constructedListType) as IEnumerable<IPersistentObject>;
            }

            // find all Entities by ID
            var persistentResults = FindIPersistentObjects(propertyType, value) as IEnumerable<object>;
            if (persistentResults.IsEmpty())
            {
                return model; //
            }

            // filter only items, which are already not included in the Model instances
            var newItems = persistentResults.Where(entity => !model.Contains(entity)).ToList();

            // extend the list with new entities
            foreach (var newItem in newItems)
            {
                model.GetType().GetMethod("Add").Invoke(model, new[] { newItem });
            }
            return model;
        }
        /// <summary>
        /// Iterates the rawValue array, calls ReadFacade to get the IPersistent object by its ID.
        /// Supported are ValueType and string IDs
        /// </summary>
        /// <param name="propertyType"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        protected virtual IEnumerable<IPersistentObject> FindIPersistentObjects(Type propertyType, ValueProviderResult value)
        {
            Contract.Requires(propertyType.Is());
            Contract.Requires(value.Is());

            var rawValueArray = value.RawValue as IEnumerable<string>;
            var values = rawValueArray.Is()
                        ? new List<string>(rawValueArray)
                        : new List<string> { value.AttemptedValue };

            var resultSet = FindIPersistentObjectsWithTypedId<int>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<decimal>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<Guid>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<long>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<double>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<float>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<short>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<ushort>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<sbyte>(propertyType, values)
                   ?? FindIPersistentObjectsWithTypedId<byte>(propertyType, values)
                   ?? FindIPersistentObjectsWithStringId(propertyType, values);

            return resultSet;
        }
        /// <summary>
        /// call to ReadFacade if ID is a struct - to find IPersistent instance
        /// </summary>
        /// <typeparam name="TID"></typeparam>
        /// <param name="propertyType"></param>
        /// <param name="values"></param>
        /// <returns></returns>
        protected virtual IEnumerable<IPersistentObject> FindIPersistentObjectsWithTypedId<TID>(Type propertyType, IEnumerable<string> values)
            where TID : struct
        {
            Contract.Requires(propertyType.Is());
            Contract.Requires(values.Is());

            var facade = GetFacade<TID>(propertyType);
            if (facade.IsNull())
            {
                return null;
            }

            // result(s) wrapped in a list (even for one Entity)
            var list = new List<IPersistentObjectWithTypedId<TID>>();
            foreach (var attemptedValue in values)
            {
                TID id; // let's try to get the value from Request.Param
                if (attemptedValue.TryParse(out id, CultureInfo.InvariantCulture))
                {
                    list.Add(facade.GetById(id)); // the facade and id are found, they are now responsible...
                }
            }

            // only not null will be returned
            return list.Where(i => i.Is());
        }
        /// <summary>
        /// call to ReadFacade if ID is a struct - to find IPersistent instance
        /// </summary>
        /// <param name="propertyType"></param>
        /// <param name="values"></param>
        /// <returns></returns>
        protected virtual IEnumerable<IPersistentObject> FindIPersistentObjectsWithStringId(Type propertyType, IEnumerable<string> values)
        {
            Contract.Requires(propertyType.Is());
            Contract.Requires(values.Is());

            var facade = GetFacade<string>(propertyType);
            if (facade.IsNull())
            {
                return null;
            }

            // result(s) wrapped in a list (even for one Entity)
            var list = new List<IPersistentObjectWithTypedId<string>>();

            foreach (var attemptedValue in values)
            {
                list.Add(facade.GetById(attemptedValue)); // the facade and string id are found, they are now responsible...
            }

            // only not null will be returned
            return list.Where(i => i.Is());
        }
        /// <summary>
        /// Calls the IReadFacade.CreateNew() and returns new instance of a pr
        /// </summary>
        /// <param name="entityType"></param>
        /// <returns></returns>
        protected virtual IPersistentObject CreateIPersistentObject(Type entityType)
        {
            return CreateIPersistentObjectsWithTypedId<int>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<decimal>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<Guid>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<long>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<double>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<float>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<short>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<ushort>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<sbyte>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<byte>(entityType)
                   ?? CreateIPersistentObjectsWithTypedId<string>(entityType);
        }
        /// <summary>
        /// Tries to create IReadFacade for ID of a TID type. 
        /// If found, CreateNew() is called
        /// </summary>
        /// <typeparam name="TID"></typeparam>
        /// <param name="entityType"></param>
        /// <returns></returns>
        protected virtual IPersistentObject CreateIPersistentObjectsWithTypedId<TID>(Type entityType)
        {
            Contract.Requires(entityType.Is());

            var facade = GetFacade<TID>(entityType);
            if (facade.IsNull())
            {
                return null;
            }

            return facade.CreateNew();
        }
        #endregion Resolve Persistent

        #region GetFacade
        protected virtual IReadFacade<IPersistentObjectWithTypedId<TID>, TID> GetFacade<TID>(Type entityType)
        {
            var persistent = typeof(IPersistentObjectWithTypedId<>).MakeGenericType(typeof(TID));
            if (!entityType.GetInterfaces().Any(i => i.IsEquivalentTo(persistent)))
            {
                return null;
            }

            // the covariant type of the filtered facade
            var type = typeof(IReadFacade<,>).MakeGenericType(entityType, typeof(TID));

            // the instance of this covariant type
            var facadeInstance = Factory.CreateInstance(type);

            // the typed instance
            var facade = facadeInstance as IReadFacade<IPersistentObjectWithTypedId<TID>, TID>;
            if (facade.IsNull())
            {
                return null;
            }
            return facade;
        }
        #endregion

        #region SetProperty
        protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext,
            PropertyDescriptor propertyDescriptor, object value)
        {
            if (value.IsNull())
            {
                if (bindingContext.ModelName.IsEmpty()
                 && bindingContext.ValueProvider.GetValue(propertyDescriptor.Name).IsNull())
                {
                    return;
                }
                // EntityA.EntityB.EntityC
                var inner = (bindingContext.ModelName ?? string.Empty) + "." + propertyDescriptor.Name;
                if (bindingContext.ValueProvider.ContainsPrefix(inner)
                    && bindingContext.ValueProvider.GetValue(inner).IsNull())
                {
                    return;
                }
                var type = propertyDescriptor.PropertyType;
                if (type.IsEquivalentTo(typeof(byte[]))) // file uploaded already
                {
                    return;
                }

                if (type.IsEquivalentTo(typeof(string)) // file name set already
                     && propertyDescriptor.Name.EndsWith("FileName"))
                {
                    return;
                }
            }
            base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
        }
        #endregion SetProperty

        #region GetModelProperties override

        protected override PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            // extend current interface properties with all inherited interfaces properties
            if (bindingContext.ModelType.IsInterface)
            {
                return GetInterfaceProperties(bindingContext.ModelType);
            }

            return base.GetModelProperties(controllerContext, bindingContext);
        }

        /// <summary>
        /// Returns all properties of a given interface type, including inherited properties
        /// </summary>
        /// <param name="interfaceType">Interface type</param>
        /// <returns>List of properties</returns>
        private static PropertyDescriptorCollection GetInterfaceProperties(Type interfaceType)
        {
            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException("Interface type expected");
            }

            var propertyDescriptors = new List<PropertyDescriptor>();

            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(interfaceType))
            {
                propertyDescriptors.Add(prop);
            }

            foreach (var type in interfaceType.GetInterfaces())
            {
                foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(type))
                {
                    propertyDescriptors.Add(prop);
                }
            }

            return new PropertyDescriptorCollection(propertyDescriptors.ToArray());
        }

        #endregion
    }
}